﻿
namespace Patterns4Net.Core
{
    using System;
    using System.Collections.Generic;
    using System.ComponentModel.Composition;
    using System.Linq;
    using System.Reflection;
    using Mono.Cecil;
    using Patterns4Net.Core.CecilExtensions;
    using Patterns4Net.Attributes;
    using Patterns4Net.Attributes.DesignPatterns;

    /// <summary>
    /// This provider locates those patterns that are tightly coupled with their attributes, 
    /// in other words this provider searches for attributes that implement the 
    /// <see cref="IPattern"/> interface directly.
    /// </summary>
    /// <remarks>
    /// <para>Tight coupling of the attribute and the pattern is not required, 
    /// however, is much easier to implement.</para>
    /// <para>
    /// Patterns4Net internally uses other mechanism of location of patterns based on attributes,
    /// which is implemented by class <see cref="PlainAttributesPatternsProvider"/>.
    /// </para>
    /// </remarks>
    [Immutable]
    [Export(typeof(IPatternsProvider))]
    public sealed class AttributesPatternsProvider : IPatternsProvider
    {
        private const string AttributeClassNotFound =
            "Patterns4Net found an attribute of type {0}, which is pattern attribute, " + 
            "but this type was not fond in loaded assemblies with plugins (loaded assemblies: {1})." +
            "Make sure that assembly that contains {0} is listed in Patterns4Net configuration.";

        private const string NoSuitableCtorFound =
            "Attribute of type {0} (found on {1}) does not have constructor with one parameter of type IDictionary<string, object> " +
            "this constructor is required in order to construct the attribute from metadata. To fix this implements a " +
            "constructor {0}(IDictionary<string, object> values) in the type {0} and reconstruct the attribute's values " + 
            "from the dictionary";

        private const string CtorException =
            "Constructor of {0} type has thrown an exception of type {1}. Message: {2}.";

        [ImportingConstructor]
        public AttributesPatternsProvider()
        {
            this.PluginAssemblies = new Assembly[0];
        }

        [Import("AttributesPatternsProviderAssemblies")]
        public Assembly[] PluginAssemblies { get; set; }

        public IEnumerable<IPattern> GetPatterns<TMember>(TMember member) where TMember : IMemberDefinition
        {            
            var patternAttributes = member.GetAttributes<IPattern>(true);
            foreach (var attribute in patternAttributes)
            {
                var typeName = CecilToReflection.GetReflectionTypeName(attribute.AttributeType.FullName);
                var attrType = this.PluginAssemblies
                    .Select(x => x.GetType(typeName, false))
                    .FirstOrDefault(x => x != null);

                if (attrType == null)
                {
                    throw new PatternCreationException(
                        string.Format(
                            AttributeClassNotFound, 
                            typeName, 
                            string.Join(", ", this.PluginAssemblies.Select(x => x.FullName))));
                }

                var ctor = attrType.GetConstructor(new[] { typeof(Dictionary<string, object>) });
                if (ctor == null)
                {
                    throw new PatternCreationException(
                        string.Format(NoSuitableCtorFound, attribute.AttributeType.FullName, member));
                }

                var parameters = new Dictionary<string, object>();
                parameters.Add("-Target", member);
                for (int i = 0; i < attribute.ConstructorArguments.Count; i++)
                {
                    parameters.Add(i.ToString(), attribute.ConstructorArguments[i].Value);
                }

                if (attribute.HasProperties)
                {
                    attribute.Properties.ForEach(x => parameters.Add(x.Name, x.Argument.Value));
                }

                IPattern pattern = null;
                try
                {
                    pattern = (IPattern)ctor.Invoke(new object[] { parameters });
                }
                catch (TargetInvocationException ex)
                {
                    throw new PatternCreationException(string.Format(CtorException, ex.GetType(), ex.Message));
                }

                yield return pattern;
            }
        }

        public class PatternCreationException : ApplicationException
        {
            public PatternCreationException(string message)
                : base(message)
            {
            }
        }
    }
}
